// Copyright (C) 2010, Scott W. Dunlop <swdunlop@gmail.com>
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//    * Redistributions of source code must retain the above copyright
//      notice, this list of conditions and the following disclaimer.
//    * Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//    * Neither the name of the <organization> nor the
//      names of its contributors may be used to endorse or promote products
//      derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
// ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#define NOSPEX_IMPL_SOURCE // Indicates that this is an implementation file.

#include "nospex.h"
#include "util.h"

#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <assert.h>
#include <cairo.h>

//-------------------------------------------------------- PRIVATE DECLARATIONS
void nx_unlink_info( nx_info_p info );
void nx_unlink_node( nx_node_p node );
void nx_unlink_graph( nx_graph_p graph );
nx_pointer nx_alloc( size_t size );

#define SET_NEXT( e, n) \
    (((nx_element_p)(e))->next) = (nx_element_p)(n);

#define SET_PREV( e, p) \
    (((nx_element_p)(e))->prev) = (nx_element_p)(p);

#define RET_IF_USED( r ) \
    if(((nx_resource_p)(r))->use_ct) return;

#define FREE_IF_UNUSED( rsrc ) \
    RET_IF_USED( rsrc ); nx_free_resource( (nx_resource_p) rsrc );

//------------------------------------------------------------- IMPLEMENTATIONS
nx_pointer nx_alloc( size_t size ){
    nx_pointer ptr = (nx_pointer)malloc( size );
    if( ptr == NULL ){
        fprintf( stderr, "Out of memory, allocating %i bytes.\n", (int)size );
        abort( );
    }else{
        memset( ptr, 0, size );
    }
    return ptr;
}

char* nx_resource_kind_name( int kind ){
    switch( kind ){
    case NX_GRAPH_KIND: return "graph";
    case NX_INFO_KIND: return "info";
    case NX_NODE_KIND: return "node";
    case NX_LINK_KIND: return "link";
    case NX_BLIP_KIND: return "blip";
    default: return "resource";
    };
};

nx_resource_p nx_create_resource( int kind, unsigned short size ){
    nx_resource_p rsrc = nx_alloc( size );
    rsrc->kind = kind;
    rsrc->red = rsrc->blue = rsrc->green = .9;
    return rsrc;
}

void nx_free_resource( nx_resource_p rsrc ){
    free( rsrc );
}

void nx_signal_and_deinfo( nx_element_p elem ){
    if( elem->info ) nx_unlink_info( elem->info );
    nx_signal_resource( (nx_resource_p)elem, NX_ELEMENT_UNLINK_EVT, 0.0, 0.0 );
}

double nx_calc_pitch( int width, int height ){
    int size = width > height ? height : width;
    return size / NX_DOMAIN_SIZE;
}

nx_graph_p nx_create_graph( ){
    nx_graph_p graph = (nx_graph_p)nx_create_resource( 
        NX_GRAPH_KIND, sizeof( struct nx_graph_t ) 
    );
    
    graph->clock_skew = nx_system_clock( ); 
    graph->pitch = nx_calc_pitch( 600, 600 ); 
    graph->count = 0;
    graph->ptr_x = -100.0; // Prevents accidental dispatching.
    graph->ptr_y = -100.0; // Prevents accidental dispatching.

    //Necessary, since Graph is not a linked component. 
    nx_add_use( (nx_resource_p)graph ); 

    nx_set_resource_color( (nx_resource_p) graph, 0.0, 0.0, 0.0 );
    return graph;
}

void nx_unlink_graph( nx_graph_p graph ){
    nx_node_p node, next_node;
    nx_signal_graph( graph, NX_GRAPH_UNLINK_EVT, 0.0, 0.0 );
    
    node = graph->first_node;
    while( node != NULL ){
        next_node = NEXT( node );
        nx_unlink_node( node );
        node = next_node;
    };

    nx_link_p link, next_link;
    
    link = graph->first_link;
    while( link != NULL ){
        next_link = NEXT( link );
        nx_unlink_link( link );
        link = next_link;
    };

    nx_blip_p blip, next_blip;
    
    blip = graph->first_blip;
    while( blip != NULL ){
        next_blip = NEXT( blip );
        nx_unlink_blip( blip );
        blip = next_blip;
    };
    
    //TODO: Kill the widget reference.
    FREE_IF_UNUSED( graph );
}

nx_node_p nx_create_node( nx_graph_p graph ){
    nx_node_p node, last_node;
    
    // Create a new node. 
    node = (nx_node_p)nx_create_resource( 
        NX_NODE_KIND, sizeof( struct nx_node_t ) 
    );
    
    // Update the graph;
    node->graph = graph;
    last_node = graph->last_node;
    SET_PREV( node, last_node );
    if( last_node ){
        SET_NEXT( last_node, node );
    }else{
        graph->first_node = node;
    };
    graph->last_node = node;
    graph->count += 1;
    
    // Initialize position 
    nx_set_random_node_position( node );
    
    // Initialize the color. 
    nx_set_random_resource_color( (nx_resource_p) node );
    
    return node;
}

nx_link_p nx_create_link( nx_node_p src, nx_node_p dst ){
    nx_graph_p graph;
    nx_link_p link, last_link;
    
    // Create a new link. 
    link = (nx_link_p)nx_create_resource( 
        NX_LINK_KIND, sizeof( struct nx_link_t ) 
    );

    assert( src->graph == dst->graph ); 
    graph = src->graph;

    link->src = src; link->dst = dst;
    
    // Update the graph;
    last_link = graph->last_link;
    SET_PREV( link, last_link );
    if( last_link ){
        SET_NEXT( last_link, link );
    }else{
        graph->first_link = link;
    };

    graph->last_link = link;

    return link;
}

nx_info_p nx_create_info( nx_element_p elem, const char* text, int len ){
    //TODO: Defensive coding, watch for excessive text lengths here.
    nx_info_p info = (nx_info_p)nx_create_resource( 
        NX_INFO_KIND, sizeof( struct nx_info_t )  + len
    );
    
    // Constraint, there is only one info per element. 
    if( elem->info ) nx_unlink_info( elem->info );
    
    info->elem = elem;
    elem->info = info; 
    memcpy( info->data, text, len );
    info->data[len] = 0;

    return info;
}

void nx_unlink_info( nx_info_p info ){
    if( info->elem ){
        info->elem->info = NULL;
        info->elem = NULL;
    };

    if( info->cache != NULL ){
        cairo_surface_destroy( info->cache );
        info->cache = NULL;
    };
    
    // Not an element, so no signal and deinfo.
    FREE_IF_UNUSED( info );
}

nx_blip_p nx_create_blip( nx_link_p link, int origin, int duration ){
    nx_graph_p graph;
    nx_blip_p blip, last_blip;
    
    // Create a new blip. 
    blip = (nx_blip_p)nx_create_resource( 
        NX_BLIP_KIND, sizeof( struct nx_blip_t ) 
    );

    graph = link->src->graph;
    blip->link = link;
    blip->origin = origin;
    
    //TODO: This is not best practice for ensuring everything ends at the same
    //      millsecond.

    nx_time now = nx_graph_clock( graph );
    blip->expiry = now + duration;
    blip->pitch = ((double)1.0) / duration;
    
    // Update the graph;
    last_blip = graph->last_blip;
    SET_PREV( blip, last_blip );
    if( last_blip ){
        SET_NEXT( last_blip, blip );
    }else{
        graph->first_blip = blip;
    };

    graph->last_blip = blip;

    return blip;
}

void nx_resource_color( 
    nx_resource_p resource, double* red, double* green, double* blue 
){
    if( resource == NULL ) return;
    if( red ) *red = resource->red;
    if( blue ) *blue = resource->blue;
    if( green ) *green = resource->green;
}

void nx_set_random_resource_color( nx_resource_p resource ){
    nx_set_resource_color( 
        resource,
        nx_random_pct( ) * 0.8 + 0.2,
        nx_random_pct( ) * 0.8 + 0.2,
        nx_random_pct( ) * 0.8 + 0.2
    );
}

void nx_set_resource_color( 
    nx_resource_p resource, double red, double green, double blue  
){
    resource->red = red;
    resource->blue = blue;
    resource->green = green;
}

void nx_set_random_node_position( nx_node_p node ){
    nx_set_node_position( 
        node,
        nx_random_pct( ) * NX_DOMAIN_SIZE,
        nx_random_pct( ) * NX_DOMAIN_SIZE
    );
}

void nx_set_node_position( nx_node_p node, double x, double y ){
    if(( node->x == x )&&( node->y == y )) return;
    node->x = x;
    node->y = y;
    node->vx = 0.0;
    node->vy = 0.0;
}

int nx_element_position( nx_element_p element, double* x, double* y ){
    nx_graph_p graph;
    nx_info_p info;

    switch( element->rsrc.kind ){
    case NX_BLIP_KIND:
        graph = nx_element_graph( element );
        if( ! graph ) return 0;
        nx_calc_blip_position( 
            (nx_blip_p)element, nx_graph_clock( graph ), x, y 
        );
        return 1;
    case NX_NODE_KIND:
        if( x ) *x = ((nx_node_p)element)->x;
        if( y ) *y = ((nx_node_p)element)->y;
        return 1;
    };

    return 0;
}

void nx_unlink_node( nx_node_p node ){
    nx_graph_p graph;
    nx_node_p prev, next;
    nx_link_p link, next_link;
    
    graph = node->graph;
    if( graph == NULL ) goto not_linked;
    nx_signal_and_deinfo( (nx_element_p) node );

    link = graph->first_link;
    while( link != NULL ){
        next_link = NEXT( link );
        if(( link->src == node )||( link->dst == node )) nx_unlink_link( link );
        link = next_link;
    };

    prev = PREV( node );
    next = NEXT( node );

    if( prev == NULL ){
        graph->first_node = next;
    }else{
        SET_NEXT( prev, next );
    }

    if( next == NULL ){
        graph->last_node = prev;
    }else{
        SET_PREV( next, prev );
    }

    graph->count -= 1;
    if( graph->ptr_elem == (nx_element_p) node ){
        graph->ptr_elem = NULL;
        nx_graph_redispatch_pointer( graph );
    };
    
    // We defer this, because links need this data. 
    node->graph = NULL;

not_linked:
    FREE_IF_UNUSED( node );
}

void nx_unlink_blip( nx_blip_p blip ){
    nx_graph_p graph;
    nx_blip_p prev, next;
    nx_link_p link;
    
    link = blip->link; 
    if( link == NULL ) goto not_linked;
    nx_signal_and_deinfo( (nx_element_p) blip );

    blip->link = NULL;
    graph = link->src->graph;

    prev = PREV( blip );
    next = NEXT( blip );

    if( prev == NULL ){
        graph->first_blip = next;
    }else{
        SET_NEXT( prev, next );
    }

    if( next == NULL ){
        graph->last_blip = prev;
    }else{
        SET_PREV( next, prev );
    }
   
    if( graph->ptr_elem == (nx_element_p) blip ){
        graph->ptr_elem = NULL;
        nx_graph_redispatch_pointer( graph );
    };

not_linked:
    FREE_IF_UNUSED( blip );
}

void nx_unlink_link( nx_link_p link ){
    nx_graph_p graph;
    nx_link_p prev, next;
    nx_node_p src, dst;

    src = link->src;
    dst = link->dst;
     
    if( src == NULL ) goto not_linked;
    nx_signal_and_deinfo( (nx_element_p) link );

    graph = src->graph;
    prev = PREV( link );
    next = NEXT( link );

    if( prev == NULL ){
        graph->first_link = next;
    }else{
        SET_NEXT( prev, next );
    }

    if( next == NULL ){
        graph->last_link = prev;
    }else{
        SET_PREV( next, prev );
    }

    nx_blip_p blip, next_blip;
    
    blip = graph->first_blip;
    while( blip != NULL ){
        next_blip = NEXT( blip );
        if( blip->link == link ) nx_unlink_blip( blip );
        blip = next_blip;
    };

    if( graph->ptr_elem == (nx_element_p) link ){
        graph->ptr_elem = NULL;
        nx_graph_redispatch_pointer( graph );
    };
    
    // We defer this unlink since blips need this data to unlink.
    link->src = link->dst = NULL;

not_linked:
    FREE_IF_UNUSED( link );
}

/** The maximum density of nodes in a graph, where 1.0 is saturation. */
#define NX_MAX_DENSITY 0.3

void nx_resize_graph( nx_graph_p graph, int width, int height ){
    nx_node_p node; 
    
    width -= (NX_NODE_RADIUS << 1); 
    height -= (NX_NODE_RADIUS << 1); 

    double pitch = nx_calc_pitch( width, height );
    if( graph->pitch == pitch ) return;
    //TODO: Recalculate node_radius;
    
    graph->pitch = pitch;
    // Enforce the margins without bouncing; (it's not the node's fault!)
    nx_enforce_graph_margins( graph, 0 );
    nx_request_graph_render( graph );
}

nx_element_p nx_find_element_at( nx_graph_p graph, double x, double y ){
    nx_node_p n;
    nx_blip_p b;
    double radius, x1, x2, y1, y2;

    radius = NX_BLIP_RADIUS / graph->pitch;
    
    x1 = x - radius;
    x2 = x + radius;
    y1 = y - radius;
    y2 = y + radius;
    
    nx_time now = nx_graph_clock( graph ); 
    for( b = graph->last_blip; b != NULL; b = PREV( b ) ){
        double bx, by;
        nx_calc_blip_position( b, now, &bx, &by );
        if( x1 > bx ) continue;
        if( x2 < bx ) continue;
        if( y1 > by ) continue;
        if( y2 < by ) continue;
        
        double dx = (bx - x);
        double dy = (by - y);
        if( sqrt( dx * dx + dy * dy ) > radius ) continue;

        //TODO: He clicked on this blip!
        return (nx_element_p)b;

    };
    
    radius = NX_NODE_RADIUS / graph->pitch;
    
    x1 = x - radius;
    x2 = x + radius;
    y1 = y - radius;
    y2 = y + radius;
    
    for( n = graph->last_node; n != NULL; n = PREV( n ) ){
        if( x1 > n->x ) continue;
        if( x2 < n->x ) continue;
        if( y1 > n->y ) continue;
        if( y2 < n->y ) continue;
        
        double dx = (n->x - x);
        double dy = (n->y - y);
        if( sqrt( dx * dx + dy * dy ) > radius ) continue;

        //TODO: He clicked on this node!
        return (nx_element_p)n;

    };
    
    return NULL; 
}

void nx_convert_to_model_xy( nx_graph_p graph, double* px, double* py ){
    double x = *px;
    double y = *py;
    
    x /= graph->pitch;
    y /= graph->pitch;
        
    if( x > NX_DOMAIN_SIZE ){ 
        x = NX_DOMAIN_SIZE;
    }else if( x < 0.0 ){ 
        x = 0.0; 
    };

    if( y > NX_DOMAIN_SIZE ){ 
        y = NX_DOMAIN_SIZE; 
    }else if( y < 0.0 ){ 
        y = 0.0; 
    };

    *px = x;
    *py = y;
}

nx_time nx_graph_clock( nx_graph_p graph ){
    return ( graph->paused ? graph->paused 
                           : nx_system_clock( ) ) - graph->clock_skew;
}

void nx_unlink_resource( nx_resource_p rsrc ){
    if( IS_ELEM( rsrc ) ){
        nx_unlink_element( (nx_element_p) rsrc );
    }else if( IS_INFO( rsrc ) ){
        nx_unlink_info( (nx_info_p) rsrc );
    }else if( IS_GRAPH( rsrc ) ){
        nx_unlink_graph( (nx_graph_p) rsrc );
    }else{
        abort( );
    };
};

void nx_unlink_element( nx_element_p elem ){
    if( IS_BLIP( elem ) ){
        nx_unlink_blip( (nx_blip_p) elem );
    }else if( IS_LINK( elem ) ){
        nx_unlink_link( (nx_link_p) elem );
    }else if( IS_NODE( elem ) ){
        nx_unlink_node( (nx_node_p) elem );
    }else{
        abort( );
    };
}

nx_info_p nx_element_info( nx_element_p elem){
    if( elem == NULL ) return NULL;
    return elem->info;
};

nx_graph_p nx_element_graph( nx_element_p elem ){
    if( elem == NULL ) return NULL;

    if( IS_BLIP( elem ) ){
        return nx_element_graph( 
            (nx_element_p) ((nx_blip_p) elem)->link
        );
    }else if( IS_LINK( elem ) ){
        return nx_element_graph( 
            (nx_element_p) ((nx_link_p) elem)->src
        );
    }else if( IS_NODE( elem ) ){
        return ((nx_node_p) elem)->graph;
    }else{
        abort( );
    };
}

nx_graph_p nx_resource_graph( nx_resource_p resource ){
    if( resource == NULL ) return NULL;

    if( IS_ELEM( resource ) ){
        return nx_element_graph( (nx_element_p) resource );
    }else if( IS_INFO( resource ) ){
        return nx_element_graph( ((nx_info_p) resource)->elem );
    }else if( IS_GRAPH( resource ) ){
        return NULL;
    };
}

void nx_add_use( nx_resource_p resource ){ 
    resource->use_ct ++; 
}

void nx_remove_use( nx_resource_p resource ){ 
    if( resource->use_ct ){
        if( --resource->use_ct ) return;
    };
    
    if( nx_resource_graph( resource ) == NULL ){
        nx_unlink_resource( resource );
    }
}

void nx_change_use( nx_resource_p x, nx_resource_p y ){
    if( x == y ) return;
    if( y != NULL ) nx_add_use( y ); 
    if( x != NULL ) nx_remove_use( x );
}

int nx_resource_kind( nx_resource_p resource ){ return resource->kind; }

int nx_is_element( nx_resource_p resource ){
    return nx_resource_kind( resource ) & NX_ELEMENT_KIND;
}
nx_element_p nx_to_element( nx_resource_p resource ){
    return IS_ELEM( resource ) ? (nx_element_p)resource : NULL;
}

int nx_is_graph( nx_resource_p resource ){ return IS_GRAPH( resource ); }
nx_graph_p nx_to_graph( nx_resource_p resource ){
    return IS_GRAPH( resource ) ? (nx_graph_p)resource : NULL;
}

int nx_is_node( nx_resource_p resource ){ return IS_NODE( resource ); }
nx_node_p nx_to_node( nx_resource_p resource ){
    return IS_NODE( resource ) ? (nx_node_p)resource : NULL;
}

int nx_is_link( nx_resource_p resource ){ return IS_LINK( resource ); }
nx_link_p nx_to_link( nx_resource_p resource ){
    return IS_LINK( resource ) ? (nx_link_p)resource : NULL;
}
    
int nx_is_blip( nx_resource_p resource ){ return IS_BLIP( resource ); }
nx_blip_p nx_to_blip( nx_resource_p resource ){
    return IS_BLIP( resource ) ? (nx_blip_p)resource : NULL;
}

int nx_is_info( nx_resource_p resource ){ return IS_INFO( resource ); }
nx_info_p nx_to_info( nx_resource_p resource ){
    return IS_INFO( resource ) ? (nx_info_p)resource : NULL;
}

void nx_signal_graph( nx_graph_p graph, int id, double x, double y ){
    if( graph == NULL) return;
    if( graph->event_fn== NULL ) return;
    graph->event_fn( graph->event_cx, (nx_resource_p)graph, id, x, y );
}

void nx_signal_resource( nx_resource_p rsrc, int id, double x, double y ){
    nx_graph_p graph = nx_resource_graph( rsrc );
    if( graph == NULL) return;
    if( graph->event_fn == NULL ) return;
    graph->event_fn( graph->event_cx, (nx_resource_p)rsrc, id, x, y );
}

void nx_set_graph_callback( nx_graph_p graph, void* ctxt, nx_event_fn func ){
    graph->event_cx = ctxt; graph->event_fn = func;
}

static struct timeval initial_tv;

void nx_start_system_clock( ){
    gettimeofday( &initial_tv, NULL );
    initial_tv.tv_sec --;
}

nx_time nx_system_clock( ){
    struct timeval current_tv;
    gettimeofday( &current_tv, NULL );
    current_tv.tv_sec --;                   // Borrowing in self defense!
    current_tv.tv_usec += 1000;
    return (
        ((nx_time)(current_tv.tv_sec - initial_tv.tv_sec)) * 1000
        + (current_tv.tv_usec - initial_tv.tv_usec) / 1000
    );
}

